package me.leoray.leilog;

import android.text.TextUtils;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

/**
 * 真正负责打印的类,
 * 一个{@link LogPrinter}对应一个{@link Settings}
 * 不同的{@link Settings} 设置打印出不同风格的日志
 * Created by leoraylei on 16/8/27.
 */
public class LogPrinter {
    private Settings settings = new Settings(); //默认配置

    /**
     * 键为类名前缀,这个tag优先级**没有**通过{@link Logs#d(String, String, Object...)}这种方式设置的tag优先级高;
     * 排序顺序按照字符串的默认顺序的反向顺序
     */
    private static Map<String, Settings> tagMap = new TreeMap<>(new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return -o1.compareTo(o2);
        }
    });    //Collections.synchronizedMap()

    private static final int MAX_CACHE = 20;
    /**
     * 键为完整的类名,限制缓存大小
     */
    private static Map<String, Settings> cacheTagMap = new LinkedHashMap<String, Settings>() {
        @Override
        protected boolean removeEldestEntry(Entry<String, Settings> eldest) {
            return size() > MAX_CACHE;
        }
    }; //类名对应的Settings缓存池

    /**
     * 根据类名前缀去指定配置(包括包名),这个方法可以指定某个类或者某个包下面的特殊配置
     *
     * @param classNamePrefix
     * @param settings()
     * @return
     */
    public LogPrinter settings(String classNamePrefix, Settings settings) {
        if (classNamePrefix != null) {
            tagMap.put(classNamePrefix, settings);
        }
        return this;
    }

    Settings defaultSettings() {
        return this.settings;
    }

    /**
     * 获取配置对象
     *
     * @return
     */
    public Settings settings() {
        if (tagMap.isEmpty()) {
            return this.settings;
        } else {
            StackTraceElement[] elements = Thread.currentThread().getStackTrace();
            int offset = getStackOffset(elements);
            StackTraceElement element = elements[offset];
            String className = element.getClassName();

            if (cacheTagMap.containsKey(className)) { //读缓存
                return cacheTagMap.get(className);
            }
            Iterator<Map.Entry<String, Settings>> iterator = tagMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, Settings> entry = iterator.next();
                if (className.startsWith(entry.getKey())) {
                    return entry.getValue();
                }
            }
            return this.settings;
        }
    }

    public void v(String tag, String msg, Object... param) {
        log(Log.VERBOSE, tag, msg, null, param);
    }

    public void d(String tag, String msg, Object... param) {
        log(Log.DEBUG, tag, msg, null, param);
    }

    public void i(String tag, String msg, Object... param) {
        log(Log.INFO, tag, msg, null, param);
    }

    public void w(String tag, String msg, Object... param) {
        log(Log.WARN, tag, msg, null, param);
    }

    public void e(String tag, String msg, Object... param) {
        log(Log.ERROR, tag, msg, null, param);
    }


    public void wtf(String msg, Object... param) {
        log(Log.ASSERT, "", msg, null, param);
    }


    public void v(String tag, String msg, Throwable param) {
        log(Log.VERBOSE, tag, msg, param);
    }

    public void d(String tag, String msg, Throwable param) {
        log(Log.DEBUG, tag, msg, param);
    }

    public void i(String tag, String msg, Throwable param) {
        log(Log.INFO, tag, msg, param);
    }

    public void w(String tag, String msg, Throwable param) {
        log(Log.WARN, tag, msg, param);
    }

    public void e(String tag, String msg, Throwable param) {
        log(Log.ERROR, tag, msg, param);
    }


    public void wtf(String msg, Throwable param) {
        log(Log.ASSERT, "", msg, null, param);
    }

    public void v(String msg, Object... param) {
        log(Log.VERBOSE, "", msg, null, param);
    }

    public void d(String msg, Object... param) {
        log(Log.DEBUG, "", msg, null, param);
    }

    public void i(String msg, Object... param) {
        log(Log.INFO, "", msg, null, param);
    }

    public void w(String msg, Object... param) {
        log(Log.WARN, "", msg, null, param);
    }

    public void e(String msg, Object... param) {
        log(Log.ERROR, "", msg, null, param);
    }


    public void wtf(String tag, String msg, Object... param) {
        log(Log.ASSERT, tag, msg, null, param);
    }


    public void json(String json) {
        json("", json);
    }

    public void json(String prefixLog, String json) {
        json("", Log.DEBUG, prefixLog, json);
    }


    /**
     * 针对JSON数据可以格式化打印
     *
     * @param tag
     * @param level
     * @param prefixLog
     * @param json      如果不是合法的JSON字符串则不会格式化
     */
    public void json(String tag, int level, String prefixLog, String json) {
        String prefix = prefixLog == null ? "" : prefixLog;
        String split = "";
        String message = "";
        if (!TextUtils.isEmpty(json)) {
            json = json.trim();
//            split = json.length() > NEW_LINE_THRESHOLD ? ":\n" : ": ";
            split = prefix == "" ? "\n" : ":\n";
            if (json.startsWith("{")) {
                try {
                    JSONObject jsonObject = new JSONObject(json);
                    message = jsonObject.toString(4);
                } catch (JSONException e) {
                    e.printStackTrace();
                    message = json;
                }
            } else if (json.startsWith("[")) {
                try {
                    JSONArray jsonArray = new JSONArray(json);
                    message = jsonArray.toString(4);
                } catch (JSONException e) {
                    e.printStackTrace();
                    message = json;
                }
            } else {
                message = json;
            }
        }
        log(level, tag, prefix + split + message, null);

    }

    public void xml(String xml) {
        xml("", xml);
    }

    public void xml(String prefixLog, String xml) {
        xml("", Log.DEBUG, prefixLog, xml);
    }

    /**
     * 日志核心方法
     *
     * @param logType
     * @param tag
     * @param msg
     * @param param
     */
    private void log(int logType, String tag, String msg, Throwable t, Object... param) {
        String finalTag = tag(settings().rootTag(), tag);
        String message = message(msg, param);
        String[] finalMsg = logDecoration(logType, finalTag, message);
        boolean proxy = logProxy(logType, finalTag, finalMsg);
        if (!proxy) {
            settings().runPipelines(logType, finalTag, finalMsg, t);
        }
    }


    /**
     * 针对XML数据格式化打印
     *
     * @param tag
     * @param level
     * @param prefixLog
     * @param xml       如果不是有效的xml格式则原样输出
     */
    public void xml(String tag, int level, String prefixLog, String xml) {
        String prefix = prefixLog == null ? "" : prefixLog;
        String split = "";
        String message = "";
        if (!TextUtils.isEmpty(xml)) {
            xml = xml.trim();
//            split = xml.length() > 100 ? ":\n" : ": ";
            split = prefix == "" ? "\n" : ":\n";
            try {
                Source xmlInput = new StreamSource(new StringReader(xml));
                StreamResult xmlOutput = new StreamResult(new StringWriter());
                Transformer transformer = TransformerFactory.newInstance().newTransformer();
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
                transformer.transform(xmlInput, xmlOutput);
                message = xmlOutput.getWriter().toString().replaceFirst(">", ">\n");
            } catch (TransformerException e) {
                e.printStackTrace();
                message = xml;
            }
        }
        log(level, tag, prefix + split + message, null);

    }

    /**
     * 返回标签
     *
     * @param rootTag
     * @param tag
     * @return
     */
    private String tag(String rootTag, String tag) {
        if (rootTag == null) {
            rootTag = "";
        }
        rootTag = rootTag.trim();
        if (tag == null) {
            tag = "";
        } else {
            tag = tag.trim();
            if (!tag.equals("") && !rootTag.equals("")) {
                tag = "-" + tag;
            }
        }
        return rootTag + tag;
    }

    private String message(String message, Object... args) {
        if (TextUtils.isEmpty(message)) {
            return "";
        }
        if (args == null || args.length == 0) {
            return message.trim();
        }
        return String.format(message, args).trim();
    }

    private String[] logDecoration(int logType, String finalTag, String message) {
        String[] finalMsg;
        final LogDecoration logDecoration = settings().getLogDecoration(logType);
        if (logDecoration == null) { //没有设置日志内容装饰器
            finalMsg = new String[]{message};
        } else {
            finalMsg = logDecoration.process(logType, finalTag, message);

        }
        return finalMsg;
    }

    private boolean logProxy(int logType, String finalTag, String[] finalMsg) {
        if (settings().getLogProxy() != null) {
            if (finalMsg != null && finalMsg.length > 0) {
                if (finalMsg.length == 1) {
                    settings().getLogProxy().log(logType, finalTag, finalMsg[0]);
                }
                for (int i = 0; i < finalMsg.length; i++) {
                    settings().getLogProxy().log(logType, finalTag, finalMsg[i]);
                }
            }
            return true;
        }
        return false;
    }


    private static final int METHOD_OFFSET_BASIC = 4; //基本的方法偏移

    /**
     * 方法偏移
     *
     * @param stackTraceElements
     * @return
     */
    public static int getStackOffset(StackTraceElement[] stackTraceElements) {
        int methodOffset = METHOD_OFFSET_BASIC;
        String className = stackTraceElements[methodOffset].getClassName();
        while (className.startsWith(Logger.class.getPackage().getName())) {
            methodOffset++;
            className = stackTraceElements[methodOffset].getClassName();
        }
        return methodOffset;
    }
}
